import Vue from 'vue';
import RuntimeRoot from './RuntimeRoot.vue';

/**
 * The runtime context.
 */
export type Context = {
  /**
   * Emit events to upper components.
   *
   * @param name the name of the event.
   * @param params the parameters.
   */
  $emit(name: string, ...params: any[]): void;
};

/**
 * The plain old JSON type.
 */
type Json = null | string | number | boolean | { [key: string]: Json } | Json[];

/**
 * The abstract type of view model.
 */
export type GeneralModel = {
  [key: string]: Json;
};

/**
 * Binding of View Model.
 */
export type ViewModelBinding<VM> = (vm: VM) => Json;

/**
 * Binding of View Model & Events.
 */
export type ViewEventBinding<VM> = (
  args: any[],
  model: VM,
  ctx: Context,
) => void;

/**
 * The binding between static view and the view model.
 */
export type ViewTemplate<VM extends GeneralModel> = {
  /**
   * The component type.
   */
  type: string;
  /**
   * The optional component name, also acted as the unique key to track reactive updates.
   */
  name?: string;
  /**
   * The properties.
   */
  props?: {
    [key: string]: Json | ViewModelBinding<VM>;
  };
  /**
   * The event handlers.
   */
  on?: Record<string, ViewEventBinding<VM>>;
  /**
   * The conventional parameters used by the layout of containers.
   */
  layout?: Record<string, Json>;
  /**
   * The sub elements, generally used by containers.
   */
  elements?: Array<ViewTemplate<VM>>;
};

const internal = Symbol('internal');

/**
 * The component type.
 */
class Component<Model extends GeneralModel> {
  [internal]: Vue.ComponentOptions<Vue>;
  constructor(readonly comp: Vue.ComponentOptions<Vue>) {
    this[internal] = comp;
  }
}

/**
 * Declare a component.
 *
 * @param template the template binding.
 * @param model the view model.
 * @returns a declarative component.
 */
export function component<Model extends GeneralModel>(
  template: ViewTemplate<Model>,
): Component<Model> {
  return new Component({
    name: 'RuntimeComponent',
    inject: {
      runtime: {
        default: null,
      },
    },
    props: {
      model: {
        type: Object,
        default() {
          return {};
        },
      },
    },
    render(h) {
      return h(RuntimeRoot, {
        props: {
          template,
          model: this.model,
        },
      });
    },
  });
}

/**
 * Render the given component to the DOM.
 *
 * @param dom the DOM element or a selector.
 * @param comp the component.
 * @param model the optional model.
 * @returns the reactive context.
 */
export function render<Model extends GeneralModel>(
  dom: string | Element,
  comp: Component<Model> | ViewTemplate<Model>,
  model?: Model,
) {
  const { [internal]: actualComp } =
    comp instanceof Component ? comp : component(comp);
  return new Vue({
    render(h) {
      return h(actualComp, {
        props: {
          model,
        },
      });
    },
  }).$mount(dom);
}

/**
 * Create a localized component context.
 *
 * @param ctx the runtime options.
 * @returns a localized component factory function.
 */
export function componentContext(ctx: {
  componentMap?: { [key: string]: Vue.Component };
}): typeof component {
  return (template) => {
    return new Component({
      name: 'ContextualRuntimeComponent',
      provide: {
        runtime: ctx,
      },
      props: {
        model: {
          type: Object,
          default() {
            return {};
          },
        },
      },
      render(h) {
        return h(component(template)[internal], {
          props: {
            model: this.model,
          },
        });
      },
    });
  };
}

/**
 * Create a localized rendering context.
 *
 * @param ctx the runtime options.
 * @returns a localized render function.
 */
export function renderContext(ctx: {
  componentMap?: { [key: string]: Vue.Component };
}): typeof render {
  const component = componentContext(ctx);
  return (dom, comp, model) => {
    return render(
      dom,
      comp instanceof Component ? comp : component(comp),
      model,
    );
  };
}

/**
 * Make sth observable by components.
 */
export const observable = Vue.observable;
